﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using Xceed.Silverlight.Data;

namespace Xceed.Silverlight.DataGrid
{
  public partial class DataGridControl
  {
    private abstract class UpdateManagerBase
    {
      protected UpdateManagerBase(DataGridControl dataGridControl)
      {
        this.DataGridControl = dataGridControl;
      }

      public bool HasPendingRequest { get { return m_pendingRequest != null; } }
      internal UpdateRequest PendingRequest { get { return m_pendingRequest; } }

      protected DataGridControl DataGridControl { get; private set; }

      protected abstract Orientation ContainerLayoutOrientation { get; }

      protected abstract void UpdateScrollPosition( SourcePositionInfo sourcePosition );
      protected abstract SourcePositionInfo ApplyReferenceUpdate( Size availableSize, ReferenceUpdateRequest request );
      protected abstract void CommitUpdate();
      protected abstract void RollbackUpdate();
      protected abstract ViewportInfo GetViewportInfo();
      protected abstract void InvalidateMeasure();
      protected abstract object GetNavigationCurrent();
      protected abstract void SetCurrentElement( object element );
      protected abstract int? NavigateCurrent( NavigationOperation navigation, out object newCurrent );
      protected abstract int? NavigateToElement( object element );
      protected abstract Size MeasureContainer( Size availableSize, ViewportPosition viewPosition );
      protected abstract bool IsStubElement( object element );


      public UpdateRequest NavigateToFirst()
      {
        return this.NavigateFirstLast( ScrollEventType.First );
      }

      public UpdateRequest NavigateToLast()
      {
        return this.NavigateFirstLast( ScrollEventType.Last );
      }

      private UpdateRequest NavigateFirstLast( ScrollEventType type )
      {
        Debug.Assert( type == ScrollEventType.First || type == ScrollEventType.Last );
        UpdateRequest request = new ScrollUpdateRequest( true, type );
        request.UpdateDetails.CurrentOnCompletion = (type == ScrollEventType.First) 
          ? ViewReferenceElement.ViewFirst 
          : ViewReferenceElement.ViewLast;

        return request;
      }

      public UpdateRequest Navigate( NavigationOperation navigation )
      {
        object currentElement = this.GetNavigationCurrent();

        if( currentElement == null )
          return null;

        Debug.Assert( navigation == NavigationOperation.NextItem
          || navigation == NavigationOperation.NextPage
          || navigation == NavigationOperation.PreviousPage
          || navigation == NavigationOperation.PreviousItem );


        object newCurrentElement;
        int? compareResult = this.NavigateCurrent( navigation, out newCurrentElement );

        if( this.IsStubElement( newCurrentElement ) )
          throw new DataGridInternalException();


        if( compareResult == 0 )
        {
          if( newCurrentElement != null )
          {
            this.SetCurrentElement( newCurrentElement );
          }
          return null;
        }
        else
        {
          return this.NavigateCurrent( navigation, currentElement, compareResult );
        }
      }

      private UpdateRequest NavigateCurrent( 
        NavigationOperation navigation,
        object currentElement,
        int? elementPositionHint )
      {
        bool isPageOperation = navigation.IsPage();

        TransitionHint transitionHint = TransitionHint.Update;
        if( elementPositionHint != null )
        {
          transitionHint = ( elementPositionHint.Value == -1 ) ? TransitionHint.ScrollBackward : TransitionHint.ScrollForward;
        }
        else 
        {
          transitionHint = TransitionHint.Update;
        }

        FetchDirection layoutDirection;
        long skip;
        ViewReferenceElement current;
        bool shouldInitialize;


        if( isPageOperation )
        {
          shouldInitialize = false;
          layoutDirection = ( navigation == NavigationOperation.PreviousPage )
            ? FetchDirection.Backward
            : FetchDirection.Forward;
          skip = 0;
          current = navigation.IsIncrement() ? ViewReferenceElement.ViewLast : ViewReferenceElement.ViewFirst;
        }
        else
        {
          shouldInitialize = true;
          skip = ( navigation == NavigationOperation.NextItem ) ? 1 : -1;

          if( elementPositionHint.GetValueOrDefault( -1 ) == -1 )
          {
            layoutDirection = FetchDirection.Forward;
            current = ViewReferenceElement.ViewFirst;
          }
          else
          {
            layoutDirection = FetchDirection.Backward;
            current = ViewReferenceElement.ViewLast;
          }
        }

        ReferenceUpdateRequest request = new ReferenceUpdateRequest( currentElement, skip, layoutDirection );
        request.UpdateDetails.TransitionHint = transitionHint;
        request.UpdateDetails.CurrentOnCompletion = current;
        request.UpdateDetails.RowPreload = isPageOperation;
        request.ShouldInitialize = shouldInitialize;

        return request;
      }

      public void BringIntoView( object element, bool updateCurrent, bool updateSelection )
      {
        if( updateSelection && !updateCurrent )
          throw new InvalidOperationException( "Cannot update the selection of an element if it is not set as current" );

        ReferenceUpdateRequest request = null;
        SelectionUpdate selectionUpdate = (updateSelection) ? new SelectionUpdate( true, true, false ) : null;

        int? viewPosition = this.NavigateToElement( element );
        if( viewPosition != 0 )
        {
          FetchDirection fetchDirection;
          TransitionHint hint;
          ViewReferenceElement currentToSet;

          if( viewPosition == null )
          {
            fetchDirection = FetchDirection.Forward;
            hint = TransitionHint.Update;
            currentToSet = ViewReferenceElement.ViewFirst;
          }
          else if( viewPosition.Value < 0 )
          {
            fetchDirection = FetchDirection.Forward;
            hint = TransitionHint.ScrollBackward;
            currentToSet = ViewReferenceElement.ViewFirst;
          }
          else
          {
            Debug.Assert( viewPosition.Value > 0 );
            fetchDirection = FetchDirection.Backward;
            hint = TransitionHint.ScrollForward;
            currentToSet = ViewReferenceElement.ViewLast;
          }

          request = new ReferenceUpdateRequest( element, 0, fetchDirection );
          request.UpdateDetails.TransitionHint = hint;

          if( updateCurrent )
          {
            request.UpdateDetails.CurrentOnCompletion = currentToSet;
          }

          if( updateSelection )
          {
            Debug.Assert( this.GetType() == typeof( RowUpdateManager ) );
            request.UpdateDetails.SelectionUpdateToApply = selectionUpdate;
          }

          this.Update( request );
        }
        else
        {
          if( updateCurrent )
          {
            this.SetCurrentElement( element );
          }

          if( updateSelection )
          {
            this.DataGridControl.ApplySelectionUpdate( selectionUpdate );
          }
        }
      }

      public virtual void Update( UpdateRequest newInfo )
      {
        if( m_pendingRequest != null )
        {
          if( ( m_pendingRequest is ScrollUpdateRequest )
            && ( newInfo is ScrollUpdateRequest ) )
          {
            ScrollUpdateRequest pendingScroll = ( ScrollUpdateRequest )m_pendingRequest;
            ScrollUpdateRequest newScroll = ( ScrollUpdateRequest )newInfo;
            if( pendingScroll.EventType == ScrollEventType.ThumbTrack
              && newScroll.EventType == ScrollEventType.ThumbTrack )
            {
              newScroll.Count += pendingScroll.Count;
            }
          }

          if( !m_pendingRequest.UpdateDetails.ColumnSizeOrPositionOnly )
          {
            newInfo.UpdateDetails.ColumnSizeOrPositionOnly = false;
          }
        }

        m_pendingRequest = newInfo;
        this.InvalidateMeasure();
      }

      private ReferenceUpdateRequest ConvertUpdateRequest( UpdateRequest updateInfo )
      {
        if( updateInfo == null )
          throw new ArgumentNullException( "updateInfo" );

        if( updateInfo is ReferenceUpdateRequest )
          return ( ReferenceUpdateRequest )updateInfo;

        if( updateInfo is ScrollUpdateRequest )
          return this.ConvertScrollUpdateRequest( ( ScrollUpdateRequest )updateInfo );


        Debug.Assert( updateInfo.GetType() == typeof( UpdateRequest ) );

        object referencePosition = null;
        ViewportPosition firstItemPosition = this.GetViewportFirstElementPosition();
        if( firstItemPosition != null )
        {
          referencePosition = firstItemPosition.Item;
        }

        ReferenceUpdateRequest converted = new ReferenceUpdateRequest( 
          updateInfo.UpdateDetails, 
          referencePosition, 
          FetchDirection.Forward );

        if( firstItemPosition != null )
        {
          converted.ReferenceOffscreenLength = firstItemPosition.OffscreenPositionLength;
        }

        return converted;
      }

      public Size OnContainerMeasureOverride( Size availableSize )
      {
        Size measuredSize = Size.Empty;
        bool firstPass = true;
        ReferenceUpdateRequest update = null;
        UpdateDetails updateDetails = null;
        ViewportPosition viewportPosition = null;
        SourcePositionInfo newPositionInfo = null;

        if( this.HasPendingRequest )
        {
          update = this.ConvertUpdateRequest( this.PendingRequest );
          m_pendingRequest = null;
        }

        while( firstPass || update != null )
        {
          firstPass = false;

          if( update != null )
          {
            updateDetails = update.UpdateDetails;
            newPositionInfo = this.ApplyReferenceUpdate( availableSize, update );

            bool alignToTop = update.FetchDirection.IsForward();
            OffsetLength offscreenLength = update.ReferenceOffscreenLength;

            if( offscreenLength.IsPixels )
            {
              if( newPositionInfo.Item == null
                || !newPositionInfo.Item.Equals( update.FetchPositionReference ) )
              {
                offscreenLength = OffsetLength.Zero;
              }
            }

            viewportPosition = new ViewportPosition( newPositionInfo.Item, alignToTop, offscreenLength );
          }
          else
          {
            viewportPosition = this.GetViewportFirstElementPosition();
          }

          measuredSize = this.MeasureContainer( availableSize, viewportPosition );

          update = this.CheckForReUpdate( availableSize, measuredSize, newPositionInfo, updateDetails );

          if( update == null && updateDetails != null )
          {
            this.ApplyCurrent( updateDetails.CurrentOnCompletion );

            SelectionUpdate selectionUpdate = updateDetails.SelectionUpdateToApply;
            if( selectionUpdate != null )
            {
              Debug.Assert( this.GetType() == typeof( RowUpdateManager ) );
              this.DataGridControl.ApplySelectionUpdate( selectionUpdate );
            }
          }

          if( update != null )
          {
            this.RollbackUpdate();
          }
          else
          {
            this.CommitUpdate();
          }
        }

        this.UpdateScrollPosition( newPositionInfo );

        return measuredSize;
      }

      protected virtual void ApplyCurrent( ViewReferenceElement? newCurrent )
      {
        if( newCurrent == null )
          return;

        object element = this.GetCurrentReferenceElement( newCurrent.Value );

        if( !this.IsStubElement( element ) )
        {
          this.SetCurrentElement( element );
        }
      }

      protected ReferenceUpdateRequest CheckForReUpdate( Size availableSize, Size measuredSize, SourcePositionInfo newPositionInfo, UpdateDetails appliedUpdateDetails )
      {
        ViewportInfo viewInfo = this.GetViewportInfo();
        bool isAtMinPosition = ( newPositionInfo != null && newPositionInfo.IsAtSourceFirst && ( viewInfo.FirstElementOffscreenLength == 0d ) );
        bool isAtMaxPosition = ( newPositionInfo != null && newPositionInfo.IsAtSourceLast && ( viewInfo.LastElementOffscreenLength == 0d ) );
        double availableLength = availableSize.GetOrientedLength( this.ContainerLayoutOrientation );
        double measuredLength = measuredSize.GetOrientedLength( this.ContainerLayoutOrientation );

        if( isAtMinPosition && isAtMaxPosition )
          return null;

        // If the current rendered elemnts is not enough for the new viewport height, add a pending update
        // in order to fill completly the viewport.
        ReferenceUpdateRequest updateAgain = null;

        if( availableLength > measuredLength )
        {
          FetchDirection fetchDirection;
          object referencePosition = null;
          OffsetLength offscreenLength = OffsetLength.Zero;

          if( isAtMaxPosition )
          {
            fetchDirection = FetchDirection.Backward;
            referencePosition = null;
          }
          else
          {

            fetchDirection = FetchDirection.Forward;
            ViewportPosition details = this.GetViewportFirstElementPosition();
            referencePosition = details.Item;
            offscreenLength = details.OffscreenPositionLength;
          }

          if( appliedUpdateDetails != null )
          {
            updateAgain = new ReferenceUpdateRequest( appliedUpdateDetails, referencePosition, fetchDirection );
          }
          else 
          {
            updateAgain = new ReferenceUpdateRequest( false, referencePosition, fetchDirection );
          }

          updateAgain.ReferenceOffscreenLength = offscreenLength;
        }

        return updateAgain;
      }

      private ReferenceUpdateRequest ConvertScrollUpdateRequest( ScrollUpdateRequest info )
      {
        Debug.Assert( info.EventType != ScrollEventType.ThumbPosition );
        Debug.Assert( info.EventType != ScrollEventType.EndScroll );
        ReferenceUpdateRequest request;

        if( info.EventType.IsFirst() || info.EventType.IsLast() )
        {
          FetchDirection direction = ( info.EventType == ScrollEventType.First ) 
            ? FetchDirection.Forward 
            : FetchDirection.Backward;

          request = new ReferenceUpdateRequest( info.UpdateDetails, null, direction );
          return request;
        }

        FetchDirection layoutDirection;
        long skip;
        OffsetLength offscreenLength = OffsetLength.Zero;
        ILayoutElement referenceElement;
        ViewportInfo viewInfo = this.GetViewportInfo();


        if( info.EventType.IsThumbTrack() )
        {
          Debug.Assert( info.Count.HasValue );
          layoutDirection = FetchDirection.Forward;
          double percentOffscreenLength = viewInfo.FirstElementOffscreenPercentLength;
          double newCount = info.Count.Value + percentOffscreenLength;
          double roundedSkip = Math.Floor( newCount );
          offscreenLength = new OffsetLength( newCount - roundedSkip, false );
          skip = Convert.ToInt64( roundedSkip );
          referenceElement = viewInfo.FirstElement;
        }
        else
        {
          Debug.Assert( info.EventType.IsIncrement() || info.EventType.IsDecrement() );

          if( info.EventType.IsDecrement() )
          {
            if( info.EventType.IsSmallChange() )
            {
              referenceElement = viewInfo.GetUnitNavigationReference( true );
              skip = -1;
            }
            else
            {
              ILayoutElement firstElement = viewInfo.GetPageNavigationReference( true );
              ILayoutElement lastElement = viewInfo.GetPageNavigationReference( false );

              bool isSameElement = object.Equals( firstElement, lastElement );

              referenceElement = firstElement;
              skip = isSameElement ? -1 : 0;
            }
          }
          else
          {
            if( info.EventType.IsSmallChange() )
            {
              referenceElement = viewInfo.GetUnitNavigationReference( false );
              skip = 1;
            }
            else
            {
              ILayoutElement firstElement = viewInfo.GetPageNavigationReference( true );
              ILayoutElement lastElement = viewInfo.GetPageNavigationReference( false );

              bool isSameElement = object.Equals( firstElement, lastElement );

              referenceElement = lastElement;
              skip = isSameElement ? 1 : 0;
            }
          }

          layoutDirection = ( info.EventType == ScrollEventType.SmallDecrement || info.EventType == ScrollEventType.LargeIncrement )
            ? FetchDirection.Forward
            : FetchDirection.Backward;
        }

        object referenceObject = ( referenceElement == null ) ? null : referenceElement.ReferenceItem;
        ReferenceUpdateRequest convertedRequest = 
          new ReferenceUpdateRequest( info.UpdateDetails, referenceObject, skip, layoutDirection );
        convertedRequest.ReferenceOffscreenLength = offscreenLength;

        return convertedRequest;
      }

      private object GetCurrentReferenceElement( ViewReferenceElement position )
      {
        ViewportInfo info = this.GetViewportInfo();
        return info.GetCurrentElementReference(position == ViewReferenceElement.ViewFirst);
      }

      protected virtual ViewportPosition GetViewportFirstElementPosition()
      {
        return null;
      }

      protected LayoutFactoryBase GetLayoutFactory( 
        TransitionHint? transitionHint )
      {
        if( transitionHint.HasValue )
        {
          if( transitionHint.Value == TransitionHint.Update )
          {
            LinearLayoutFactory baseFactory = new LinearLayoutFactory( this.ContainerLayoutOrientation );
            return new UpdateTransitionFactory( baseFactory );
          }
          else
          {
            bool scrollForward = ( transitionHint.Value == DataGrid.TransitionHint.ScrollForward );
            TransitionContext context = this.DataGridControl.GetTransitionContext( null );
            return new ScrollLayoutFactory( scrollForward, this.ContainerLayoutOrientation );
          }
        }
        else
        {
          return new LinearLayoutFactory( this.ContainerLayoutOrientation );
        }
      }

      protected void UpdatePositionInformation( ScrollBar scrollBar, SourcePositionInfo positionInfo )
      {
        if( this.HasPendingRequest )
          return;

        ViewportInfo viewInfo = this.GetViewportInfo();

        double? offset = null;
        ReferencePoint? offsetReferencePoint = null;
        double? totalCount = null;
        bool? offsetIsEstimate = null;
        bool? totalCountIsEstimate = null;

        if( positionInfo != null )
        {
          if( positionInfo.ItemOffset != null )
          {
            offset = ( double )( positionInfo.ItemOffset.EstimateValue - 1 ).Value;

            offsetReferencePoint = positionInfo.ItemOffset.ReferencePosition;
            offsetIsEstimate = positionInfo.ItemOffset.IsEstimate;

            Debug.Assert( offset != -1 || viewInfo.NbVisibleElements == 0 );
            if( viewInfo.NbVisibleElements == 0 )
            {
              offset = 0d;
            }
            else
            {
              bool referToFirst = object.Equals( positionInfo.Item, viewInfo.FirstElement.ReferenceItem );
              bool referToLast = !referToFirst && object.Equals( positionInfo.Item, viewInfo.LastElement.ReferenceItem );
              bool reversedReference = (offsetReferencePoint.Value == ReferencePoint.AfterLast);
              Debug.Assert( referToFirst || referToLast );

              if(referToLast)
              {
                offset -= (!reversedReference) 
                  ? ( viewInfo.Elements.Count - 1 )  
                  : -( viewInfo.Elements.Count - 1 );
              }

              offset += (!reversedReference) 
                ? viewInfo.FirstElementOffscreenPercentLength
                : -viewInfo.FirstElementOffscreenPercentLength;
            }
          }

          if( positionInfo.TotalCount != null )
          {
            totalCount = ( double )positionInfo.TotalCount.Value;
            totalCountIsEstimate = positionInfo.TotalCount.IsEstimate;
          }
        }

        this.UpdateScrollBarValues( 
          scrollBar, 
          viewInfo.NbVisibleElements, 
          offset, 
          offsetIsEstimate, 
          offsetReferencePoint, 
          totalCount, 
          totalCountIsEstimate );
      }

      private void UpdateScrollBarValues(
        ScrollBar scrollBar,
        double viewportLength,
        double? offset,
        bool? offsetIsEstimate,
        ReferencePoint? offsetReferencePoint,
        double? totalCount,
        bool? totalCountIsEstimate )
      {
        if( offset == null )
        {
          offset = scrollBar.Value;
          offsetReferencePoint = ReferencePoint.BeforeFirst;
          offsetIsEstimate = true;
        }

        if( totalCount == null )
        {
          totalCount = scrollBar.ViewportSize + ( scrollBar.Maximum - scrollBar.Minimum );
          totalCountIsEstimate = true;
        }

        // Handle any possible inconsistency between offset, totalCount and viewportSize
        if( offset < 0d )
        {
          Debug.Assert( offsetIsEstimate.Value );
          offset = 0d;
        }

        if( offsetReferencePoint == ReferencePoint.BeforeFirst )
        {
          if( ( offset + viewportLength ) > totalCount )
          {
            if( totalCountIsEstimate.Value )
            {
              totalCount = ( offset + viewportLength );
            }
            else
            {
              offset = totalCount - viewportLength;
            }
          }
        }
        else
        {
          if( offset >= totalCount )
          {
            if( totalCountIsEstimate.Value )
            {
              totalCount = offset + 1;
            }
            else
            {
              if( offset != 0d )
              {
                offset = totalCount - 1;
              }
            }
          }
        }

        double newValue = ( offsetReferencePoint == ReferencePoint.BeforeFirst )
        ? offset.Value
        : Math.Max( 0d, totalCount.Value - 1 ) - offset.Value;

        scrollBar.ViewportSize = viewportLength;
        scrollBar.Minimum = 0;
        scrollBar.Maximum = (totalCount.Value - viewportLength);
        scrollBar.Value = newValue;
      }

      private UpdateRequest m_pendingRequest;

      protected class SourcePositionInfo
      {
        public SourcePositionInfo( bool isAtSourceFirst, bool isAtSourceLast, object item, RelativeOffset itemOffset, EstimateInt64 totalCount )
        {
          this.IsAtSourceFirst = isAtSourceFirst;
          this.IsAtSourceLast = isAtSourceLast;
          this.Item = item;
          this.ItemOffset = itemOffset;
          this.TotalCount = totalCount;
        }

        public object Item { get; private set; }
        public RelativeOffset ItemOffset { get; private set; }
        public EstimateInt64 TotalCount { get; private set; }
        public bool IsAtSourceFirst { get; private set; }
        public bool IsAtSourceLast { get; private set; }
      }

    }
  }
}
